package io.gitee.macxiang.utils;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/** 微信相关,使用的话,最好采用继承的方式,然后使用init方法进行初始化配置 */
public class WeChat {

  /** api基础域名 */
  private final String apiBaseUrl = "https://api.mch.weixin.qq.com";
  /** 商户号 */
  private String merchantId;
  /** 商户私钥 */
  private PrivateKey privateKey;
  /** 商户证书序列号 */
  private String merchantSerialNumber;
  /** 商户平台证书序列号 */
  private String merchantSerialPubNumber;
  /** 商户平台证书 */
  private PublicKey merchantSerialPub;
  /** 商户APIV3密钥 */
  private byte[] apiV3Key = null;
  /** 小程序appid */
  private String mpAppid, mpSearch;
  /** 公众号appid 密钥 */
  private String gzhAppid, gzhSearch;
  /** 转账场景 */
  private String transferScene;
  /** 域名 */
  protected String domain;

  //#region getter
  public PrivateKey getPrivateKey() {
    if (privateKey == null) {
      throw new RuntimeException("商户私钥 为空");
    }
    return privateKey;
  }

  public String getMerchantId() {
    if (Datas.isEmpty(merchantId)) {
      throw new RuntimeException("商户号 为空");
    }
    return merchantId;
  }

  public String getMerchantSerialNumber() {
    if (Datas.isEmpty(merchantSerialNumber)) {
      throw new RuntimeException("商户证书序列号 为空");
    }
    return merchantSerialNumber;
  }

  public byte[] getApiV3Key() {
    if (apiV3Key == null) {
      throw new RuntimeException("商户APIV3密钥 为空");
    }
    return apiV3Key;
  }

  public String getMpAppid() {
    if (Datas.isEmpty(mpAppid)) {
      throw new RuntimeException("小程序appid 为空");
    }
    return mpAppid;
  }

  public String getMpSearch() {
    if (Datas.isEmpty(mpSearch)) {
      throw new RuntimeException("小程序search 为空");
    }
    return mpSearch;
  }

  public String getGzhAppid() {
    if (Datas.isEmpty(gzhAppid)) {
      throw new RuntimeException("公众号appid 为空");
    }
    return gzhAppid;
  }

  public String getGzhSearch() {
    if (Datas.isEmpty(gzhSearch)) {
      throw new RuntimeException("公众号search 为空");
    }
    return gzhSearch;
  }

  public String getDomain() {
    if (Datas.isEmpty(domain)) {
      throw new RuntimeException("本站域名 为空");
    }
    return domain;
  }

  public String getMerchantSerialPubNumber() {
    if (Datas.isEmpty(merchantSerialPubNumber)) {
      throw new RuntimeException("商户平台证书序列号 为空");
    }
    return merchantSerialPubNumber;
  }

  public PublicKey getMerchantSerialPub() {
    if (merchantSerialPub == null) {
      throw new RuntimeException("商户平台证书 为空");
    }
    return merchantSerialPub;
  }

  public String getTransferScene() {
    if (Datas.isEmpty(transferScene)) {
      throw new RuntimeException("转账场景 为空");
    }
    return transferScene;
  }
  //#endregion getter结束

  /**初始化
   * @param merchantId              商户号
   * @param privateKey              商户私钥
   * @param merchantSerialNumber    商户证书序列号
   * @param apiV3Key                商户APIV3密钥
   * @param mpAppid                 小程序appid
   * @param mpSearch                小程序Search
   * @param gzhAppid                公众号appid
   * @param gzhSearch               公众号Search
   * @param domain                  域名
   * @param merchantSerialPubNumber 商户平台证书序列号
   * @param merchantSerialPub       商户平台证书
   * @param transferScene           转账场景
   */
  public void init(String merchantId, String privateKey, String merchantSerialNumber, String apiV3Key,
      String mpAppid, String mpSearch,
      String gzhAppid, String gzhSearch,
      String domain,
      String merchantSerialPubNumber,
      String merchantSerialPub,
      String transferScene) {
    this.merchantId = merchantId;
    if (Datas.isEmpty(privateKey)) {
      this.privateKey = null;
    } else {
      byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey.replace("-----BEGIN PRIVATE KEY-----", "")
          .replace("-----END PRIVATE KEY-----", "")
          .replaceAll("\\s", ""));
      // 生成PrivateKey对象
      PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
      try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        this.privateKey = keyFactory.generatePrivate(keySpec);
      } catch (Exception e) {
        throw new RuntimeException("处理商户私钥错误:" + merchantSerialNumber);
      }
    }

    this.merchantSerialNumber = merchantSerialNumber;
    if (!Datas.isEmpty(apiV3Key)) {
      this.apiV3Key = apiV3Key.getBytes();
      if (this.apiV3Key.length != 32) {
        this.apiV3Key = null;
      }
    }
    this.mpAppid = mpAppid;
    this.mpSearch = mpSearch;
    this.gzhAppid = gzhAppid;
    this.gzhSearch = gzhSearch;
    this.domain = Datas.toDomain(domain);
    this.merchantSerialPubNumber = merchantSerialPubNumber;
    if (Datas.isEmpty(merchantSerialPub)) {
      this.merchantSerialPub = null;
    } else {
      byte[] b = Base64.getDecoder().decode(merchantSerialPub.replaceAll("-+[ A-Z]+-+|[\s\r\n]+", ""));
      try {
        // this.merchantSerialPub = new SecretKeySpec(b, "AES");
        // PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(b);
        // KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        // this.merchantSerialPub = keyFactory.generatePrivate(keySpec);

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        java.security.cert.Certificate certificate = certificateFactory
            .generateCertificate(new ByteArrayInputStream(b));
        this.merchantSerialPub = certificate.getPublicKey();

      } catch (Exception e) {
        // throw new RuntimeException("处理商户平台证书错误:" + merchantSerialPubNumber);
        utils.out(e);
      }
    }
    this.transferScene = transferScene;
  }

  private String Authorization(String nonce_str, String timestamp, String signature) {
    return String.format(
        "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%s\",serial_no=\"%s\",signature=\"%s\"",
        getMerchantId(), nonce_str, timestamp, getMerchantSerialNumber(), signature);
  }

  /** 小程序登录 */
  public String mpCode(String code) {
    if (Datas.isEmpty(code)) {
      throw new RuntimeException("code 不能为空");
    }
    JSONObject p = new JSONObject();
    p.put("appid", getMpAppid());
    p.put("secret", getMpSearch());
    p.put("js_code", code);
    p.put("grant_type", "authorization_code");
    String s = null;
    p = Http.getJson("https://api.weixin.qq.com/sns/jscode2session", p);
    if (p.containsKey("unionid")) {
      s = p.getString("unionid");
    } else if (p.containsKey("openid")) {
      s = p.getString("openid");
    }
    return s;
  }

  /** 公众号登录 */
  public JSONObject gzhCode(String code) {
    if (Datas.isEmpty(code)) {
      throw new RuntimeException("code 不能为空");
    }
    JSONObject ret = null;
    try {
      ret = Http.getJson("https://api.weixin.qq.com/sns/oauth2/access_token", JSONObject.of(
          "appid", getGzhAppid(),
          "secret", getGzhSearch(),
          "code", code,
          "grant_type", "authorization_code"));
    } catch (Exception e) {
      Datas.print("公众号登录 错误", Datas.errString(e));
    }
    if (ret == null) {
      throw new RuntimeException("公众号登录错误");
    }
    return ret;
  }

  /**公众号登录 */
  public JSONObject gzhCode(String code, boolean getUserInfo) {
    JSONObject ret = gzhCode(code);
    if (getUserInfo) {
      String access_token = ret.getString("access_token"),
          openid = ret.getString("openid");
      if (!Datas.isEmpty(access_token) && !Datas.isEmpty(openid)) {
        ret.putAll(Http.getJson("https://api.weixin.qq.com/sns/userinfo", JSONObject.of(
            "access_token", access_token,
            "openid", openid,
            "lang", "zh_CN")));
      }
    }
    return ret;
  }

  /** 签名函数 */
  private String sign(String str) {
    try {
      Signature s = Signature.getInstance("SHA256withRSA", "BC");
      s.initSign(getPrivateKey());
      s.update(str.getBytes());
      byte[] signedBytes = s.sign();
      return Base64.getEncoder().encodeToString(signedBytes);
    } catch (Exception e) {
      throw new RuntimeException("签名错误-" + Datas.errString(e));
    }
  }

  /** 获取回调地址 */
  private final String getNotifyUrl(String notify) {
    if (Datas.isEmpty(notify)) {
      notify = "notify_wx";
    }
    final String URL_PATTERN = "^https?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
    if (Pattern.matches(URL_PATTERN, notify)) {
      return notify;
    }
    return String.format("https://%s/%s", getDomain(), notify.replaceAll("^/+", ""));
  }

  /** 生成请求签名 需要商户号 */
  private String genBodySign(String method, String url, String body) {
    String timestamp = Times.sTime(), nonceStr = Datas.sGetId32(),
        s = String.join("\n", Arrays.asList(method, url, timestamp, nonceStr, body, ""));
    s = sign(s);
    s = Authorization(nonceStr, timestamp, s);
    return s;
  }

  /** 解密回调
   * @param serial 请求头 Wechatpay-Serial
   * @param signature 请求头 Wechatpay-Signature
   * @param timestamp 请求头 Wechatpay-Timestamp
   * @param nonce 请求头 Wechatpay-Nonce
   * @param body 请求内容 body
   * @return 成功返回解密后的内容,失败 抛出异常;
   */
  private JSONObject deNotify(String serial, String signature, long timestamp, String nonce, JSONObject body) {
    JSONObject resource = body.getJSONObject("resource");
    // 开始验签
    if (!getMerchantSerialPubNumber().equals(serial)) {
      throw new RuntimeException("证书ID错误");
      // 您应先检查 HTTP 头 Wechatpay-Serial 的内容是否跟商户当前所持有的微信支付平台证书的序列号一致。若不一致，请重新获取证书。否则，签名的私钥和证书不匹配，将验证失败。
    }
    if ((Times.time() - timestamp) > 300l) { // 超过5分钟
      System.out.println("验签超时");
      Datas.print("body内容", body.toJSONString());
      Datas.print("Wechatpay-Serial", serial); // 验签的微信支付平台证书序列号/微信支付公钥ID
      Datas.print("Wechatpay-Signature", signature); // 验签的签名值
      Datas.print("Wechatpay-Timestamp", timestamp); // 验签的时间戳
      Datas.print("Wechatpay-Nonce", nonce); // 验签的随机字符串
      throw new RuntimeException("验签超时");
    }
    String str = String.format("%s\n%s\n%s\n", timestamp, nonce, body.toJSONString());
    try {
      Signature s = Signature.getInstance("SHA256withRSA", "BC");
      s.initVerify(getMerchantSerialPub());
      s.update(str.getBytes());
      if (!s.verify(Base64.getDecoder().decode(signature))) {
        throw new RuntimeException();
      }
    } catch (Exception e) {
      throw new RuntimeException("验签失败");
    }
    // 验签结束
    try {
      str = resource.getString("associated_data");
      byte[] ciphertext = resource.getBytes("ciphertext"),
          associatedData = str == null ? null : str.getBytes();
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      GCMParameterSpec spec = new GCMParameterSpec(128, resource.getString("nonce").getBytes());
      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(this.apiV3Key, "AES"), spec);
      cipher.updateAAD(associatedData);
      resource = Datas.toJsonObject(new String(cipher.doFinal(ciphertext), "utf-8"));
    } catch (Exception e) {
      throw new RuntimeException("包体数据错误");
    }
    return resource;
  }

  public Jsapi jsapi = new Jsapi();

  public class Jsapi {
    /** 发起支付 忽略回调,使用公众号
     * @param des127 订单说明 127字节
     * @param id32 订单id
     * @param total 总金额(分)
     * @param openid 用户openid
     * @return 调起支付数据
     */
    public JSONObject create(String des127, String id32, Integer total, String openid) {
      return create(des127, id32, total, openid, null, null);
    }

    /** 发起支付
     * @param des127 订单说明 127字节
     * @param id32 订单id
     * @param total 总金额(分)
     * @param openid 用户openid
     * @param notify 回调地址
     * @param isMp 是否小程序
     * @return 调起支付数据
     */
    public JSONObject create(String des127, String id32, Integer total, String openid, String notify, Boolean isMp) {
      final String api = "/v3/pay/transactions/jsapi",
          appid = isMp != null && isMp ? getMpAppid() : getGzhAppid();
      int len = id32.getBytes(StandardCharsets.UTF_8).length;
      if (len > 32 || len < 6) {
        throw new RuntimeException("id长度错误");
      }
      JSONObject body = new JSONObject();
      body.put("appid", appid);
      body.put("mchid", getMerchantId());
      body.put("description", Datas.trimToByteLength(des127, 120) + "...");
      body.put("out_trade_no", id32);
      /** 支付结束时间 */
      body.put("time_expire", Times.l2times(Times.time() + 300, "yyyy-MM-dd'T'HH:mm:ssXXX"));
      /** 支付回调 */
      body.put("notify_url", getNotifyUrl(notify));
      // 金额
      body.put("amount", JSONObject.of("total", total, "currency", "CNY"));
      // 用户信息
      body.put("payer", JSONObject.of("openid", openid));
      JSONObject ret = Http.postJson(apiBaseUrl + api, body,
          JSONObject.of("Authorization", genBodySign("POST", api, body.toJSONString()),
              "Accept", "application/json",
              "Content-Type", "application/json"));
      if (ret != null) {
        String prepay_id = ret.getString("prepay_id");
        if (Datas.isEmpty(prepay_id)) {
          throw new RuntimeException(
              ret != null && ret.containsKey("message") ? ret.getString("message") : "支付错误,请稍后再试");
        }
        prepay_id = "prepay_id=" + prepay_id;
        String timeStamp = Times.sTime(),
            nonceStr = Datas.sGetId32(),
            s = String.format("%s\n%s\n%s\n%s\n", appid, timeStamp, nonceStr, prepay_id);
        ret.put("timeStamp", timeStamp);
        ret.put("nonceStr", nonceStr);
        ret.put("package", prepay_id);
        ret.put("signType", "RSA");
        ret.put("paySign", sign(s));
        ret.put("appId", appid);
        return ret;
      }
      throw new RuntimeException("生成支付数据错误");
    }

    /** 回调
     * @param request
     * @param response
     * @return null时,校验失败,{amount:"总金额",out_trade_no:"单号id",state:"对应的trade_state/refund_status",type:"pay/refund/transfer/other"}
     */
    public JSONObject notify(HttpServletRequest request, HttpServletResponse response) {
      if (response == null || request == null) {
        throw new RuntimeException("参数错误");
      }
      JSONObject p = Datas.request2json(request), a;
      try {
        p = deNotify(request.getHeader("Wechatpay-Serial"),
            request.getHeader("Wechatpay-Signature"),
            Long.valueOf(request.getHeader("Wechatpay-Timestamp")).longValue(),
            request.getHeader("Wechatpay-Nonce"),
            p);
        a = p.getJSONObject("amount");
        if (p.containsKey("trade_state")) {
          p.put("id", p.getString("out_trade_no"));
          p.put("amount", a.getLongValue("payer_total"));
          p.put("type", "pay");
          p.put("state", p.getString("trade_state"));
        } else if (p.containsKey("refund_status")) {
          p.put("id", p.getString("out_trade_no"));
          // p.put("id", p.getString("out_refund_no"));
          p.put("amount", a.getLongValue("total"));
          p.put("type", "refund");
          p.put("state", p.getString("refund_status"));
        } else if (p.containsKey("transfer_amount")) {
          p.put("id", p.getString("out_bill_no"));
          p.put("amount", p.getLongValue("transfer_amount"));
          p.put("type", "transfer");
          // p.put("state", p.getString("state")); // 重名无需重新赋值
        } else {
          p.put("type", "other");
          p.put("state", "error");
        }
        response.setStatus(200);
      } catch (Exception e) {
        System.out.println("回调错误");
        e.printStackTrace();
        p = null;
        response.setStatus(406);
        response.setContentType("application/json");
        try {
          response.getWriter().write(JSONObject.of(
              "code", "FAIL",
              "message", Datas.errString(e)).toJSONString());
        } catch (Exception ee) {
          System.out.println("写入包体失败:" + Datas.errString(ee));
        }
      }

      return p;
    }

    /** 查询订单
     * @param id 要查询的客户订单id
     * @return 如果订单已支付,则返回订单金额,退款返回0,未支付返回null;
     */
    public Integer query(String id) {
      final String api = String.format("/v3/pay/transactions/out-trade-no/%s?mchid=%s", id, getMerchantId()),
          timestamp = Times.sTime(), nonceStr = Datas.sGetId32(),
          signature = sign(String.format("GET\n%s\n%s\n%s\n\n", api, timestamp, nonceStr));
      try {
        JSONObject res = Http.getJson(apiBaseUrl + api, null, JSONObject.of(
            "Authorization", Authorization(nonceStr, timestamp, signature),
            "Accept", "application/json"));
        switch (res.getString("trade_state")) {
          case "SUCCESS":
            return res.getJSONObject("amount").getIntValue("payer_total");
          case "REFUND":
            return 0;
          default:
            break;
        }
      } catch (Exception e) {
      }
      return null;
    }

    /**查询退款
    * @param id 退款id
    * @return 退款成功返回金额,未查询到返回null;
    */
    public Integer queryRefund(String id) {
      final String api = String.format("/v3/refund/domestic/refunds/re%s", id),
          timestamp = Times.sTime(), nonceStr = Datas.sGetId32(),
          signature = sign(String.format("GET\n%s\n%s\n%s\n\n", api, timestamp, nonceStr));
      try {
        JSONObject res = Http.getJson(apiBaseUrl + api, null, JSONObject.of(
            "Authorization", Authorization(nonceStr, timestamp, signature),
            "Accept", "application/json"));
        String status = res.getString("status");
        if (status != null) {
          return status.equals("SUCCESS") ? res.getJSONObject("amount").getIntValue("refund") : 0;
        }
      } catch (Exception e) {
      }
      return null;
    }

    /**退款
     * @param id 要退款的id
     * @param amount 退款金额, 如果传入null或小于1 则会进行查询
     * @param reason 退款说明
     * @param notify 回调地址
     * @return 成功返回金额,失败(未支付或已退款)返回null,退款异常、等待或其他需要手动处理的返回0,也可重复发起
     */
    public Integer refund(String id, Integer amount, String reason, String notify) {
      Integer total = amount != null && amount > 0 ? amount : query(id);
      if (total != null && total > 0) {
        final String api = "/v3/refund/domestic/refunds";
        JSONObject body = JSONObject.of(
            "out_trade_no", id, // 商户订单号
            "out_refund_no", "re" + id, // 商户退款单号
            "reason", reason, // 退款原因
            "notify_url", getNotifyUrl(notify), // 回调地址
            "amount", JSONObject.of(
                "refund", total, // 退款金额
                "total", total, // 原订单总金额
                "currency", "CNY" // 退款币种
            ));
        JSONObject res = Http.postJson(apiBaseUrl + api, body,
            JSONObject.of("Authorization", genBodySign("POST", api, body.toJSONString()),
                "Accept", "application/json",
                "Content-Type", "application/json"));
        Datas.print("申请退款", res);
        return res.containsKey("status") ? total : 0;
      }
      return null;
    }

    /** 转账
     * @param id32
     * @param total 金额
     * @param openid 用户id
     * @param des32 备注
     * @param notify 回调地址
     * @param userName 用户真实姓名
     * @param scene1  岗位类型
     * @param scene2  报酬说明
     * @param isMp    是否小程序
     * @return
     */
    public JSONObject transfer(String id32, int total, String openid, String des32, String notify,
        String userName, String scene1, String scene2, Boolean isMp) {
      final String api = "/v3/fund-app/mch-transfer/transfer-bills",
          appid = isMp != null && isMp ? getMpAppid() : getGzhAppid();
      if (total > 9) {
        JSONObject body = JSONObject.of(
            "appid", appid,
            "out_bill_no", id32,
            "transfer_scene_id", getTransferScene(),
            "openid", openid,
            "transfer_amount", total
        // 
        );
        body.put("transfer_remark", Datas.trimToByteLength(des32, 32));
        body.put("notify_url", getNotifyUrl(notify));
        switch (getTransferScene()) { // 不同转账场景 报备信息不同
          case "1000": // 现金营销
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "岗位类型", "info_content", scene1),
                JSONObject.of("info_type", "报酬说明", "info_content", scene2)));

            break;
          case "1005": // 佣金报酬
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "岗位类型", "info_content", scene1),
                JSONObject.of("info_type", "报酬说明", "info_content", scene2)));
          case "企业赔付": // 企业赔付
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "赔付原因", "info_content", scene1)));
          case "采购货款": // 采购货款
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "采购商品名称", "info_content", scene1)));
          case "二手回收": // 二手回收
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "回收商品名称", "info_content", scene1)));
          case "公益补助": // 公益补助
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "公益活动名称", "info_content", scene1), // 在民政部的备案名称
                JSONObject.of("info_type", "公益活动备案编号", "info_content", scene2) // 在民政部的备案编号
            ));
          case "行政补贴": // 行政补贴
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "补贴类型", "info_content", scene1)));
          case "保险理赔": // 保险理赔
            body.put("transfer_scene_report_infos", JSONArray.of(
                JSONObject.of("info_type", "保险产品备案编号", "info_content", scene1), // 
                JSONObject.of("info_type", "保险名称", "info_content", scene1), // 
                JSONObject.of("info_type", "保险操作单号", "info_content", scene2) // 
            ));
          default:
            break;
        }
        // 收款用户姓名】 收款方真实姓名。需要加密传入，支持标准RSA算法和国密算法，公钥由微信侧提供。转账金额 >= 2,000元时，该笔明细必须填写
        if (!Datas.isEmpty(userName) && total >= 200000) {
          try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, getMerchantSerialPub());
            body.put("user_name", Base64.getEncoder().encodeToString(cipher.doFinal(userName.getBytes("utf8"))));
          } catch (Exception e) {
            throw new RuntimeException("加密用户名失败:" + Datas.errString(e));
          }
        }
        try {
          JSONObject p = Http.postJson(apiBaseUrl + api, body,
              JSONObject.of("Authorization", genBodySign("POST", api, body.toJSONString()),
                  "Accept", "application/json",
                  "Content-Type", "application/json",
                  "Wechatpay-Serial", getMerchantSerialPubNumber()));
          if (p.containsKey("package_info")) {
            return JSONObject.of("mchId", getMerchantId(),
                "appId", appid,
                "package", p.getString("package_info"));
          } else {
            throw new RuntimeException(p.getString("message"));
          }
        } catch (Exception e) {
          throw new RuntimeException(Datas.errString(e));
        }
        // {"code":"NOT_ENOUGH","message":"资金不足"}
        // {"create_time":"2025-03-10T15:12:46+08:00","out_bill_no":"ttt005","state":"WAIT_USER_CONFIRM","transfer_bill_no":"1330007264033962503100076430176459","package_info":"ABBQO+oYAAABAAAAAAAVr2ws9nk3HpQ275DOZxAAAADnGpepZahT9IkJjn90+1qg8nLwvpJ7vXUTW6hy/TKm016xgxYFcC6RG0NZ7wUj5s+oawCH3WAxyJ/ChFCkANyfmtrm2l49TjoTeIsEiDis56OqTm0="}
      } else {
        throw new RuntimeException("金额太小");
      }
    }

    /**转账查询
     * @param id 转账id
     * @return 成功返回金额,否则0
     */
    public int queryTransfer(String id) {
      final String api = String.format("/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/%s", id),
          timestamp = Times.sTime(), nonceStr = Datas.sGetId32(),
          signature = sign(String.format("GET\n%s\n%s\n%s\n\n", api, timestamp, nonceStr));
      try {
        JSONObject res = Http.getJson(apiBaseUrl + api, null, JSONObject.of(
            "Authorization", Authorization(nonceStr, timestamp, signature),
            "Accept", "application/json"));
        if ("SUCCESS".equals(res.getString("status"))) {
          return res.getIntValue("transfer_amount");
        }
      } catch (Exception e) {
      }
      return 0;
    }

  }

  String accessToken = "";
  long tokenTimeOut = 0;

  private String getAccessToken() {
    if (tokenTimeOut < Times.time()) {
      JSONObject p = new JSONObject();
      p.put("appid", getMpAppid());
      p.put("secret", getMpSearch());
      // p.put("grant_type", "refresh_token");
      p.put("grant_type", "client_credential");
      p = Http.getJson("https://api.weixin.qq.com/cgi-bin/token", p);
      if (p.containsKey("access_token") && p.containsKey("expires_in")) {
        accessToken = p.getString("access_token");
        tokenTimeOut = Times.time() + p.getLongValue("expires_in");
      } else {
        throw new RuntimeException("获取微信token错误");
      }
    }
    return accessToken;
  }

  public String getMpLink(String path) {
    if (path == null) {
      return null;
    }
    JSONObject p = new JSONObject();
    // p.put("page_url", "pages/jmp"); // 通过 Short Link
    // 进入的小程序页面路径，必须是已经发布的小程序存在的页面，可携带 query，最大1024个字符
    // return
    // Datas.httpPost("https://api.weixin.qq.com/wxa/genwxashortlink?access_token=" +
    // getAccessToken(), p);
    try {
      path = java.net.URLEncoder.encode(path, "utf8");
    } catch (Exception e) {
    }

    p.put("path", "pages/jmp");
    p.put("query", "path=" + path);

    p = Http.postJson("https://api.weixin.qq.com/wxa/generate_urllink?access_token=" + getAccessToken(), p);
    return p.getString("url_link");
  }

  /**发货
   * @param orderId   订单id
   * @param itemDesc  订单说明
   * @param wxid      用户openid
   */
  public void delivery(String orderId, String itemDesc, String wxid) { // 发货
    final String url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token="
        + getAccessToken();

    JSONObject p = JSONObject.of("order_key",
        JSONObject.of("order_number_type", 1, "mchid", getMerchantId(), "out_trade_no", orderId),
        "logistics_type", 4, // 物流模式，发货方式枚举值：1、实体物流配送采用快递公司进行实体物流配送形式 2、同城配送 3、虚拟商品，虚拟商品，例如话费充值，点卡等，无实体配送形式 4、用户自提
        "delivery_mode", "UNIFIED_DELIVERY", // 发货模式，发货模式枚举值：1、UNIFIED_DELIVERY（统一发货）2、SPLIT_DELIVERY（分拆发货） 示例值: UNIFIED_DELIVERY
        "shipping_list", new JSONArray().fluentAdd(JSONObject.of("item_desc", itemDesc)),
        "payer", JSONObject.of("openid", wxid));
    p.put("upload_time", utils.timeISO8601());
    System.out.println("发货返回:" + utils.postString(url, p));
  }

}
